package org.dei.perla.sys.device.channel;

import java.util.ArrayList;

import org.dei.perla.component.Component;
import org.dei.perla.sys.device.channel.frame.FrameConfiguration;
import org.dei.perla.sys.device.channel.frame.address.FrameAddress;
import org.dei.perla.utils.logger.LogRecord.Type;
import org.dei.perla.utils.logger.LogRecord.Verbosity;
import org.dei.perla.utils.messages.AdapterChannelMessage;
import org.dei.perla.utils.pipe.Pipe;
import org.dei.perla.utils.waiter.Waitable;
import org.dei.perla.utils.waiter.Waiter;

/**
 * Questa classe rappresenta un componente Channel Manager. 
 * Un ChannelManager permette la gestione di canali molteplici (di un unica tipologia)
 * e li interfaccia con i livelli superiori.
 *
 */
abstract public class ChannelManager extends Component {

	/**
	 * Contiene un identificativo univoco del Channel Manager
	 */
	private int pChannelId;
	
	/**
	 * Contiene la configurazione dei frame
	 */
	protected FrameConfiguration pFrameConfiguration;
	
	/**
	 * Contiene i canali attivi
	 */
	protected ArrayList<Channel> pChannel;
	
	/**
	 * Contiene le informazioni di routing tra I/O e i canali appropriati
	 */
	private ArrayList<ChannelRoutingItem> pChannelRouting;
	
	/**
	 * Contiene la pipe di input, usata per ricevere dati dal ChannelManager
	 */
	private Pipe<AdapterChannelMessage> pInputPipe;
	
	/**
	 * Contiene la pipe di output, usata per inviare dati tramite il ChannelManager
	 */
	private Pipe<AdapterChannelMessage> pOutputPipe;
	
	/**
	 * Thread per effettuare il routing dalla pipe di output verso il canale corretto
	 */
	private Thread pThreadChannelRoutingOutput;
	
	/**
	 * Thread per effettuare il routing dai vari canali alla pipe di output
	 */
	private Thread pThreadChannelRoutingInput;
	
	/**
	 * Oggetto per attendere in modo non bloccante sulla pipe di output
	 */
	private Waiter pOutputPipeWaiter;
	
	/**
	 * Oggetto per attendere in modo non bloccante sulle pipe di input dei canali
	 */
	protected Waiter pChannelInputPipeWaiter;
	
	/**
	 * Costruttore, inizializza il ChannelManager
	 * 
	 * @param parChannelId Identificativo del ChannelManager
	 * @param parName Nome del ChannelManager
	 * @param parChannel Canali pre-inizializzati
	 * @param parFrameConfiguration Configurazione dei frame
	 */
	public ChannelManager(int parChannelId, String parName, ArrayList<Channel> parChannel, FrameConfiguration parFrameConfiguration) {
		super(parName);
		
		this.pChannelId					= parChannelId;
		this.pFrameConfiguration 		= parFrameConfiguration;
		
		this.pChannelRouting 			= new ArrayList<ChannelRoutingItem>();
		
		this.pOutputPipeWaiter 			= new Waiter();
		this.pChannelInputPipeWaiter 	= new Waiter();
		
		this.pChannel		= parChannel;
		
		this.pOutputPipe 	= new Pipe<AdapterChannelMessage>(parName + "_snd");
		this.pInputPipe 	= new Pipe<AdapterChannelMessage>(parName + "_rcv");
		
		this.pOutputPipeWaiter.addWaitable(this.pOutputPipe.getWaitable());
		
	}
	
	/**
	 * Restituisce il riferimento alla Pipe da utilizzare per ricevere dati dal canale
	 * @return
	 */
	public Pipe<AdapterChannelMessage> getInputPipe() {
		return this.pInputPipe;
	}
	
	
	/**
	 * Restituisce il riferimento alla Pipe da utilizzare per inviare dati sul canale
	 */
	public Pipe<AdapterChannelMessage> getOutputPipe() {
		return this.pOutputPipe;
	}

	/**
	 * Restituisce un identificativo univoco per il channel manager
	 * @return
	 */
	public int getChannelId(){
		return this.pChannelId;
	}
	
	/**
	 * Avvia il channel manager
	 */
	public void start() {
		
		// Verifica che il ChannelManager non sia già stato avviato
		if(!this.isStarted()) {

			// Avvia le Pipe di input
			this.pInputPipe.loggerDetachAll();
			this.pInputPipe.loggerAttach(this);
			this.pInputPipe.start();
			
			// Avvia la Pipe di output
			this.pOutputPipe.loggerDetachAll();
			this.pOutputPipe.loggerAttach(this);
			this.pOutputPipe.start();
			
			// Avvia i canali
			for (int i = 0; i < this.pChannel.size(); i++) {
				// Avvia il canale
				this.pRegisterChannel(this.pChannel.get(i), this.pChannel.get(i).getRemoteAddress());
			}

			// Avvia i waiter
			this.pChannelInputPipeWaiter.start();
			this.pOutputPipeWaiter.start();
			
			// Avvia il thread di routing tra la pipe di output e i canali
			this.pThreadChannelRoutingOutput = new Thread() {
				public void run() {
					pThreadChannelRoutingOutputFunction();
				}
			};
			this.pThreadChannelRoutingOutput.start();

			// Avvia il thread per il dispatching dei pacchetti tra i canali e la pipe di input
			this.pThreadChannelRoutingInput = new Thread() {
				public void run() {
					pThreadChannelRoutingInputFunction();
				}
			};
			this.pThreadChannelRoutingInput.start();
			
			// Imposta il flag che indica che il componente è avviato
			this.setStarted(true);
		}
	}
	
	/**
	 * Arresta il Channel Manager, arrestando i thread avviati
	 */
	public void stop() {
		
		// Verifica che il ChannelManager sia avviato
		if(this.isStarted()) {
		
			// Arresta i thread
			this.pOutputPipeWaiter.stop();
			try {
				this.pThreadChannelRoutingOutput.join();
			}
			catch(Exception parException) { }
			this.pThreadChannelRoutingOutput = null;
			
			this.pChannelInputPipeWaiter.stop();
			try {
				this.pThreadChannelRoutingInput.join();
			}
			catch(Exception parException) { }
			this.pThreadChannelRoutingInput = null;
	
			// Arresta le pipe
			this.pInputPipe.stop();
			this.pOutputPipe.stop();
		
			// Imposta il flag che indica che il componente è arrestato
			this.setStarted(false);
		}
	}
	
	/**
	 * Effettua il routing dei pacchetti ricevuti attraverso la Output Pipe
	 * verso il canale appropriato. Se il canale non è più esistente lo ricrea.
	 */
	@SuppressWarnings("unchecked")
	private void pThreadChannelRoutingOutputFunction() {
		
		Pipe<AdapterChannelMessage> tPipe;
		Waitable tWaitable = this.pOutputPipeWaiter.waitNext();
		
		while (tWaitable != null) {
			
			// Determina la pipe da cui prelevare il pacchetto da inviare
			tPipe = (Pipe<AdapterChannelMessage>)tWaitable.getParentComponent();
			
			// Estrae dalla pipe il pacchetto da inviare
			AdapterChannelMessage tFrameToRoute = tPipe.dequeue();
			
			// Seleziona il canale su cui inviare il pacchetto in base all'indirizzo di destinazione
			boolean tFrameRouted = false;
			for (int i = 0; i < this.pChannelRouting.size(); i++) {
				ChannelRoutingItem tTemp = this.pChannelRouting.get(i);
				if (tFrameToRoute.getAddress().equals(tTemp.getRemoteAddress())) {
					tTemp.getChannel().getOutputPipe().enqueue(tFrameToRoute);
					this.loggerNotify(Type.Information, "Inoltro pacchetto " + tFrameToRoute.toString() + " da ChannelManager.outputPipe a " + tTemp.getChannel().getName(), Verbosity.High);
					tFrameRouted = true;
					break;
				}
			}
			
			// Se non ho accodato il messaggio su qualche canale, significa che il canale è andato
			// perso e va ricreato
			if (!tFrameRouted) {
				
				// Crea un nuovo canale
				Channel tTempChannel = this.pCreateChannel(tFrameToRoute.getAddress());
				
				// Registra il canale
				this.pRegisterChannel(tTempChannel, tFrameToRoute.getAddress());
				
				// Aggiungi il canale alla lista di canali
				this.pChannel.add(tTempChannel);
				
				// Accoda il messaggio
				tTempChannel.getOutputPipe().enqueue(tFrameToRoute);
				
				this.loggerNotify(Type.Information, "Inoltro pacchetto " + tFrameToRoute.toString() + " da ChannelManager.outputPipe a " + tTempChannel.getName(), Verbosity.High);
				
			}
			
			// Attendi il prossimo pacchetto
			tWaitable = this.pOutputPipeWaiter.waitNext();
			
		}
		
	}
	
	/**
	 * Effettua l'accodamento sulla Input Pipe dei pacchetti ricevuti dai vari
	 * canali gestiti dal ChannelManager
	 */
	@SuppressWarnings("unchecked")
	private void pThreadChannelRoutingInputFunction() {
		
		Pipe<AdapterChannelMessage> tPipe;
		Waitable tWaitable = this.pChannelInputPipeWaiter.waitNext();
		
		while (tWaitable != null) {
			
			// Determina la pipe da cui prelevare il pacchetto da inviare
			tPipe = (Pipe<AdapterChannelMessage>)tWaitable.getParentComponent();
			
			// Estrae dalla pipe il pacchetto da inviare
			AdapterChannelMessage tFrameToRoute = tPipe.dequeue();
			
			// Accoda sulla pipe di input
			this.pInputPipe.enqueue(tFrameToRoute);
			
			this.loggerNotify(Type.Information, "Inoltro pacchetto " + tFrameToRoute.toString() 
					+ " da " + tPipe.getName() +" a ChannelManager.inputPipe", Verbosity.High);
			
			// Attendi il prossimo pacchetto
			tWaitable = this.pChannelInputPipeWaiter.waitNext();
			
		}
		
	}
	
	/**
	 * Crea un nuovo canale verso la macchina con indirizzo <parAddress>
	 * @param parAddress Indirizzo della macchina remota
	 * @return Restituisce il canale creato
	 */
	abstract protected Channel pCreateChannel(FrameAddress parAddress);
	
	/**
	 * Registra un nuovo canale nel channel manager
	 * @param parChannel Canale da registrare
	 * @param parRemoteAddress Indirizzo remoto della macchina all'altro lato del canale, 
	 * 							null per leggere l'informazione dal canale stesso
	 */
	protected void pRegisterChannel(Channel parChannel, FrameAddress parRemoteAddress) {
		
		// Registra il canale all'oggetto di attesa sulle input del canale
		this.pChannelInputPipeWaiter.addWaitable(parChannel.getInputPipe().getWaitable());
		
		// Registra i logger sul canale
		parChannel.loggerDetachAll();
		parChannel.loggerAttach(this);
		
		// Avvia il canale 
		parChannel.start();
		
		// Aggiungi le informazioni di routing sul canale
		if ( parRemoteAddress == null ) {
			this.pChannelRouting.add(new ChannelRoutingItem(parChannel.getRemoteAddress(), parChannel));
		} else {
			this.pChannelRouting.add(new ChannelRoutingItem(parRemoteAddress, parChannel));
		}
	}
	
	/**
	 * Contiene informazioni per il routing corretto dei pacchetti tra la pipe di output
	 * e il canale appropriato.
	 */
	private class ChannelRoutingItem {
		
		/**
		 * Contiene l'indirizzo della macchina remota
		 */
		private FrameAddress pRemoteAddress;
		
		/**
		 * Contiene il canale per collegarsi alla macchina remota
		 */
		private Channel pChannel;
		
		/**
		 * Costruttor
		 * @param parAddress
		 * @param parChannel
		 */
		public ChannelRoutingItem(FrameAddress parAddress, Channel parChannel ) {
			this.pChannel 		= parChannel;
			this.pRemoteAddress = parAddress;
		}
		
		/**
		 * Restituisce il canale
		 * @return
		 */
		public Channel getChannel() {
			return this.pChannel;
		}
		
		/**
		 * Restituisce l'indirizzo remoto
		 * @return
		 */
		public FrameAddress getRemoteAddress() {
			return this.pRemoteAddress;
		}
	}
	
	/**
	 * Crea una rappresentazione testuale del ChannelManager
	 * @return
	 */
	public String toString() {
		String s = "";
		for (int i= 0; i < this.pChannel.size(); i++ ) {
			s += this.pChannel.get(i).toString();
		}
		return s;
	}
	
}
